package com.rupertjones.globalcron.domain.scheduling;

import com.rupertjones.globalcron.domain.JobDescriptor;
import com.rupertjones.globalcron.domain.dao.JobDescriptorDAO;
import org.apache.log4j.Logger;
import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdScheduler;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.text.ParseException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static java.lang.String.format;

@Transactional
@Service
public class QuartzScheduler implements GlobalCronScheduler {

    public static final String GROUP_NAME = "global-cron";
    private final StdScheduler scheduler;
    private volatile JobDescriptorDAO jobDescriptorDao;
    private static final Logger LOG = Logger.getLogger(QuartzScheduler.class);
    private JobExecutionListener listener;
    private volatile Map<String, Integer> versions = new HashMap<String, Integer>();

    public QuartzScheduler(StdScheduler scheduler, JobDescriptorDAO jobDescriptorDao, JobExecutionListener listener) {
        this.scheduler = scheduler;
        this.listener = listener;
        this.jobDescriptorDao = jobDescriptorDao;
    }

    public boolean running(JobDescriptor job) {
        try {
            return scheduler.getTrigger(String.valueOf(job.getId()), GROUP_NAME) != null;
        } catch (SchedulerException e) {
            return false;
        }
    }

    @Override
    public Trigger getTrigger(int jobId) {
        try {
            return scheduler.getTrigger(String.valueOf(jobId), GROUP_NAME);
        } catch (SchedulerException e) {
            return null;
        }
    }

    @Override
    public boolean executeJob(int id) {
        try {
            scheduler.triggerJob(String.valueOf(id), GROUP_NAME);
            return true;
        } catch (SchedulerException e) {
            return false;
        }
    }

    @Override
    public boolean deleteJob(int id) {
        try {
            return scheduler.deleteJob(String.valueOf(id), GROUP_NAME);
        } catch (SchedulerException e) {
            return false;
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<JobExecutionContext> getCurrentlyExecutingJobs() {
        return scheduler.getCurrentlyExecutingJobs();
    }

    public void adjustForModifications() {
        adjustForModifications(false);
    }

    @Override
    public void adjustForModifications(boolean force) {
        Collection<JobDescriptor> jobs = jobDescriptorDao.getAllJobs();
        boolean adjusted = false;

        for (JobDescriptor job : jobs) {
            if (versions.containsKey(String.valueOf(job.getId()))) {
                int version = versions.get(String.valueOf(job.getId()));
                if (force || version < job.getVersion() || (!running(job) && job.canStart())) {
                    adjustJob(job);
                    adjusted = true;
                }
            } else {
                adjustJob(job);
                adjusted = true;
            }
        }

        if (adjusted) {
            printSummary(jobs);
        }
    }

    private void printSummary(Collection<JobDescriptor> jobs) {
        LOG.info("****************************************");
        LOG.info("Jobs Summary                            ");
        LOG.info("");
        for (JobDescriptor job : jobs) {
            LOG.info(job.toReportingLine());
        }
        LOG.info("****************************************");
    }

    public boolean removeJob(JobDescriptor job) {
        try {
            if (scheduler.deleteJob(String.valueOf(job.getId()), GROUP_NAME)) {
                LOG.info(format("Removing job [%s][%s]", job.getId(), job.getName()));
                return true;
            }
        } catch (SchedulerException e) {
            LOG.error(e);
            return false;
        }
        return false;
    }

    private void adjustJob(JobDescriptor job) {
        LOG.info(format("Adjusting job [%s][%s]", job.getId(), job.getName()));
        removeJob(job);
        if (job.canStart()) {
            scheduleJob(job);
        }
        versions.put(String.valueOf(job.getId()), job.getVersion());
        LOG.info(format("Job adjusted [%s][%s]", job.getId(), job.getName()));
    }

    private void scheduleJob(JobDescriptor job) {
        LOG.info(format("Scheduling job [%s][%s]", job.getId(), job.getName()));
        JobDataMap dataMap = new JobDataMap();
        dataMap.put("jobDescriptor", job);

        if (listener != null) {
            dataMap.put("jobExecutionListener", listener);
        }

        JobDetail jobDetail = new JobDetail(String.valueOf(job.getId()), GROUP_NAME, QuartzJob.class);
        jobDetail.setJobDataMap(dataMap);
        CronTrigger trigger = new CronTrigger();
        trigger.setName(String.valueOf(job.getId()));
        trigger.setJobName(String.valueOf(job.getId()));
        trigger.setGroup(GROUP_NAME);
        trigger.setJobGroup(GROUP_NAME);
        trigger.setTimeZone(job.getTimezoneAsObject().toTimeZone());
        try {
            trigger.setCronExpression(job.getCron());
            scheduler.scheduleJob(jobDetail, trigger);
            job.updateNextFire(trigger.getNextFireTime());
            jobDescriptorDao.upsert(job);
        } catch (ParseException e) {
            LOG.error("There was a problem initializing the scheduler", e);
        } catch (SchedulerException e) {
            LOG.error("There was a problem initializing the scheduler", e);
        }
    }
}
